﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

using ModelDesigner = Microsoft.Data.Entity.Design.Model.Designer;
using ModelDiagram = Microsoft.Data.Tools.Model.Diagram;

namespace Microsoft.Data.Entity.Design.Model.Designer
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Xml;
    using System.Xml.Linq;
    using Microsoft.Data.Entity.Design.Model.Commands;
    using Microsoft.Data.Entity.Design.Model.Entity;
    using Microsoft.Data.Entity.Design.Model.Eventing;
    using Microsoft.Data.Entity.Design.Model.XLinqAnnotations;
    using Microsoft.Data.Tools.XmlDesignerBase.Model;

    internal class Diagrams : EFElement
    {
        internal static readonly string ElementName = "Diagrams";

        private readonly List<Diagram> _diagrams = new List<Diagram>();

        [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        internal Diagrams(EFElement parent, XElement element)
            : base(parent, element)
        {
            // Listen to on before model changes event so we can inject commands that synches the diagram model.
            Artifact.ModelManager.BeforeModelChangesCommitted += OnBeforeModelChangesCommitted;
        }

        internal void AddDiagram(Diagram diagram)
        {
            _diagrams.Add(diagram);
        }

        internal Diagram FirstDiagram
        {
            get
            {
                if (_diagrams.Count > 0)
                {
                    return _diagrams[0];
                }
                return null;
            }
        }

        /// <summary>
        ///     Return diagram with the given id; return null if no match is found.
        /// </summary>
        /// <param name="diagramId"></param>
        /// <returns></returns>
        internal Diagram GetDiagram(string diagramId)
        {
            return _diagrams.Where(d => d.Id.Value == diagramId).FirstOrDefault();
        }

        internal IEnumerable<Diagram> Items
        {
            get { return _diagrams; }
        }

        #region overrides

        // we unfortunately get a warning from the compiler when we use the "base" keyword in "iterator" types generated by using the
        // "yield return" keyword.  By adding this method, I was able to get around this.  Unfortunately, I wasn't able to figure out
        // a way to implement this once and have derived classes share the implementation (since the "base" keyword is resolved at 
        // compile-time and not at runtime.
        private IEnumerable<EFObject> BaseChildren
        {
            get { return base.Children; }
        }

        internal override IEnumerable<EFObject> Children
        {
            get
            {
                foreach (var efobj in BaseChildren)
                {
                    yield return efobj;
                }

                foreach (EFObject efobj in _diagrams)
                {
                    yield return efobj;
                }
            }
        }

        protected override void OnChildDeleted(EFContainer efContainer)
        {
            var diagram = efContainer as Diagram;
            if (diagram != null)
            {
                _diagrams.Remove(diagram);
            }

            base.OnChildDeleted(efContainer);
        }

#if DEBUG
        internal override ICollection<string> MyChildElementNames()
        {
            var s = base.MyChildElementNames();
            s.Add(Diagram.ElementName);
            return s;
        }
#endif

        protected override void PreParse()
        {
            Debug.Assert(State != EFElementState.Parsed, "this object should not already be in the parsed state");

            ClearEFObjectCollection(_diagrams);

            base.PreParse();
        }

        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        internal override bool ParseSingleElement(ICollection<XName> unprocessedElements, XElement elem)
        {
            if (elem.Name.LocalName == Diagram.ElementName)
            {
                var diagram = new Diagram(this, elem);
                diagram.Parse(unprocessedElements);
                _diagrams.Add(diagram);
            }
            else
            {
                return base.ParseSingleElement(unprocessedElements, elem);
            }
            return true;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                Artifact.ModelManager.BeforeModelChangesCommitted -= OnBeforeModelChangesCommitted;
            }
            base.Dispose(disposing);
        }

        #endregion

        #region Diagram management

        private struct ShapeChangeInformation
        {
            internal XObjectChange ChangeType { get; set; }
            internal EFObject ModelEFObject { get; set; }
            internal string DiagramId { get; set; }
        }

        // The goal is to keep Escher and Diagram model in sync.
        // Before model changes transaction is committed, we determine whether we need to update the diagram model.
        // If yes, we will inject commands that mutates the diagram model to the transaction.
        // For example: If an association is created between 2 entity-types and the entity-types are exist in the diagram,
        // we will add CreateAssociationConnector command to the transaction.
        [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
        private void OnBeforeModelChangesCommitted(object sender, EfiChangingEventArgs e)
        {
            if (e.CommandProcessorContext != null)
            {
                var artifact = e.CommandProcessorContext.Artifact;

                if (_diagrams != null)
                {
                    // Copy the current Xml changes over since we are going to modifying the transaction
                    var xmlChangesCopy = new List<IXmlChange>();
                    var shapeChangeInfoSet = new HashSet<ShapeChangeInformation>();

                    foreach (var xmlChange in e.CommandProcessorContext.EfiTransaction.XmlChanges)
                    {
                        // if the changed node is whitespace, ignore it as we don't want to pick up false
                        // annotations for it
                        if (xmlChange.Node.NodeType == XmlNodeType.Text)
                        {
                            var text = xmlChange.Node as XText;
                            var trimmedValue = text.Value.Trim();
                            if (String.IsNullOrEmpty(trimmedValue))
                            {
                                continue;
                            }
                        }

                        // Ignore changes to annotations
                        var externalModelChange = new EFArtifact.ExternalXMLModelChange(xmlChange, artifact.ExpectEFObjectForXObject);
                        if (externalModelChange.IsAnnotationChange(artifact.GetNamespaces()))
                        {
                            continue;
                        }

                        // Construct a fast-queryable lookup for the shape change information that already
                        // exists in this transaction. The transaction may have already fired some rules that
                        // injected shape (DiagramEFObjects) information into the transaction. The purpose
                        // of this lookup is to skip duplicate additions/removals.
                        var changedEFObject = externalModelChange.ChangedEFObject;
                        if (changedEFObject != null)
                        {
                            var diagramObject = changedEFObject as ModelDiagram.BaseDiagramObject;

                            if (diagramObject != null)
                            {
                                var changedEntityTypeShape = changedEFObject as EntityTypeShape;
                                var changedAssociationConnector = changedEFObject as AssociationConnector;
                                var changedInheritanceConnector = changedEFObject as InheritanceConnector;

                                var shapeChangeInformation = new ShapeChangeInformation();
                                if (changedEntityTypeShape != null)
                                {
                                    shapeChangeInformation.ModelEFObject = changedEntityTypeShape.EntityType.Target;
                                }
                                else if (changedAssociationConnector != null)
                                {
                                    shapeChangeInformation.ModelEFObject = changedAssociationConnector.Association.Target;
                                }
                                else if (changedInheritanceConnector != null)
                                {
                                    shapeChangeInformation.ModelEFObject = changedInheritanceConnector.EntityType.Target;
                                }
                                shapeChangeInformation.ChangeType = xmlChange.Action;
                                Debug.Assert(
                                    diagramObject.Diagram != null, changedEFObject.ToPrettyString() + " doesn't belong to any diagram.");
                                if (diagramObject.Diagram != null)
                                {
                                    shapeChangeInformation.DiagramId = diagramObject.Diagram.Id;
                                }
                                shapeChangeInfoSet.Add(shapeChangeInformation);
                            }
                        }
                        xmlChangesCopy.Add(xmlChange);
                    }

                    // Call resolve on the diagram objects in the model here. When the EDMX is being incrementally updated by some
                    // external factor (for example, a database project in a by-ref edmx), there may be situations where the existing
                    // diagram objects are not resolved yet. This means that when we try to create entity type shapes or association connectors
                    // the logic assumes that there aren't any anti-dependencies of the model elements, so it adds duplicate shapes/connectors
                    // which can affect the loading of the model later (it will hit DSL diagram validation logic).
                    XmlModelHelper.NormalizeAndResolve(this);

                    // If we see that an Association, EntityType, or EntityTypeBaseType has been added in the model
                    // changes, then we create the subsequent diagram EFObjects (AssociationConnector, EntityTypeShape,
                    // and InheritanceConnector). By creating them here, this will automatically push new changes onto the
                    // Xml transaction
                    foreach (var xmlChange in xmlChangesCopy)
                    {
                        var changedEFObject = ModelItemAnnotation.GetModelItem(xmlChange.Node);
                        if (changedEFObject != null
                            && ModelHelper.GetBaseModelRoot(changedEFObject) is ConceptualEntityModel)
                        {
                            var entityType = changedEFObject as ConceptualEntityType;
                            var association = changedEFObject as Association;
                            var baseType = changedEFObject as EntityTypeBaseType;

                            foreach (var diagram in _diagrams)
                            {
                                if (entityType != null)
                                {
                                    InjectEntityTypeShapeCommand(
                                        e.CommandProcessorContext, shapeChangeInfoSet, diagram, entityType, xmlChange.Action);
                                }
                                else if (association != null)
                                {
                                    InjectAssociationConnectorCommand(
                                        e.CommandProcessorContext, shapeChangeInfoSet, diagram, association, xmlChange.Action);
                                }
                                else if (baseType != null)
                                {
                                    InjectInheritanceConnectorCommand(
                                        e.CommandProcessorContext, shapeChangeInfoSet, diagram, baseType, xmlChange.Action);
                                }
                            }
                        }
                    }
                }
            }
        }

        private static void InjectEntityTypeShapeCommand(
            CommandProcessorContext commandProcessorContext, HashSet<ShapeChangeInformation> shapeChangeInfoSet, Diagram diagram,
            ConceptualEntityType entityType, XObjectChange changeAction)
        {
            // First check to see if there is already a change in the original transaction 
            // for the EntityTypeShape that matches the one that we're getting
            var shapeChangeInfoToQuery = new ShapeChangeInformation
                {
                    ChangeType = changeAction,
                    ModelEFObject = entityType,
                    DiagramId = diagram.Id.Value
                };

            var shapeChangeInfoExists = shapeChangeInfoSet.Contains(shapeChangeInfoToQuery);

            // We only want to create model diagram if the transaction is originated from this diagram.
            if (changeAction == XObjectChange.Add)
            {
                // We only want to inject the entity-type-shape if the transaction is originated from the passed in diagram.
                if (commandProcessorContext != null
                    && commandProcessorContext.EfiTransaction != null)
                {
                    var contextItem =
                        commandProcessorContext.EfiTransaction.GetContextValue<DiagramContextItem>(
                            EfiTransactionOriginator.TransactionOriginatorDiagramId);

                    if (contextItem != null
                        && contextItem.DiagramId == diagram.Id.Value)
                    {
                        // look in the dictionary for an 'add' to an EntityTypeShape that points to this modelobject.
                        if (shapeChangeInfoExists == false)
                        {
                            var cmd = new CreateEntityTypeShapeCommand(diagram, entityType);
                            CommandProcessor.InvokeSingleCommand(commandProcessorContext, cmd);
                        }

                        // We have the ability to create an EntityType and an Inheritance in one transaction, so we need
                        // to check for it here.
                        if (entityType.BaseType.Target != null)
                        {
                            InjectInheritanceConnectorCommand(
                                commandProcessorContext, shapeChangeInfoSet, diagram, entityType.BaseType, changeAction);
                        }
                    }
                }
            }
            else if (changeAction == XObjectChange.Remove
                     && shapeChangeInfoExists == false)
            {
                // this is happening before the transaction is taking place so we are free to look up anti-dependencies
                // on this delete
                foreach (
                    var entityTypeShape in
                        entityType.GetAntiDependenciesOfType<EntityTypeShape>()
                            .Where(ets => ets.Diagram != null && ets.Diagram.Id == diagram.Id.Value))
                {
                    if (entityTypeShape != null)
                    {
                        var deleteEntityTypeShapeCommand = entityTypeShape.GetDeleteCommand();
                        CommandProcessor.InvokeSingleCommand(commandProcessorContext, deleteEntityTypeShapeCommand);
                    }
                }
            }
        }

        private static void InjectAssociationConnectorCommand(
            CommandProcessorContext commandProcessorContext, HashSet<ShapeChangeInformation> shapeChangeInfoSet, Diagram diagram,
            Association association, XObjectChange changeAction)
        {
            // First check to see if there is already a change in the original transaction 
            // for the AssociationConnector that matches the one that we're getting
            var shapeChangeInfoToQuery = new ShapeChangeInformation
                {
                    ChangeType = changeAction,
                    ModelEFObject = association,
                    DiagramId = diagram.Id.Value
                };

            var shapeChangeInfoExists = shapeChangeInfoSet.Contains(shapeChangeInfoToQuery);

            // We only want to create model diagram if the transaction is originated from this diagram.
            if (changeAction == XObjectChange.Add
                && shapeChangeInfoExists == false)
            {
                // The association connector is added if
                // - the participating entities are in the diagram.
                // or
                // - the participating entities will be added in the current transaction.
                foreach (var end in association.AssociationEnds())
                {
                    if (end.Type != null
                        && end.Type.Target != null)
                    {
                        if (diagram.EntityTypeShapes.Where(ets => ets.EntityType.Target == end.Type.Target).Any()
                            || shapeChangeInfoSet.Where(
                                sc =>
                                sc.DiagramId == diagram.Id.Value && sc.ModelEFObject == end.Type.Target
                                && sc.ChangeType == XObjectChange.Add).Any())
                        {
                            continue;
                        }
                    }
                    // If it reach this point, that means that we should not add association connector in the diagram.
                    return;
                }
                var cmd = new CreateAssociationConnectorCommand(diagram, association);
                CommandProcessor.InvokeSingleCommand(commandProcessorContext, cmd);
            }
            else if (changeAction == XObjectChange.Remove
                     && shapeChangeInfoExists == false)
            {
                // this is happening before the transaction is taking place so we are free to look up anti-dependencies on this delete
                foreach (
                    var associationConnector in
                        association.GetAntiDependenciesOfType<AssociationConnector>()
                            .Where(ac => ac.Diagram != null && ac.Diagram.Id == diagram.Id.Value))
                {
                    if (associationConnector != null)
                    {
                        var deleteAssociationConnectorCommand = associationConnector.GetDeleteCommand();
                        CommandProcessor.InvokeSingleCommand(commandProcessorContext, deleteAssociationConnectorCommand);
                    }
                }
            }
        }

        [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
        private static void InjectInheritanceConnectorCommand(
            CommandProcessorContext commandProcessorContext, HashSet<ShapeChangeInformation> shapeChangeInfoSet,
            Diagram diagram, EntityTypeBaseType baseType, XObjectChange changeAction)
        {
            // First check to see if there is already a change in the original transaction 
            // for the InheritanceConnector that matches the one that we're getting
            var shapeChangeInfoToQuery = new ShapeChangeInformation
                {
                    ChangeType = changeAction,
                    ModelEFObject = baseType.OwnerEntityType,
                    DiagramId = diagram.Id.Value
                };

            var shapeChangeInfoExists = shapeChangeInfoSet.Contains(shapeChangeInfoToQuery);

            if (changeAction == XObjectChange.Add
                && shapeChangeInfoExists == false)
            {
                var participatingEntityTypes = new List<EntityType>();

                var derivedEntityType = baseType.Parent as EntityType;
                Debug.Assert(derivedEntityType != null, "Where is the parent EntityType of this BaseType attribute?");

                if (derivedEntityType != null)
                {
                    // The inheritance connector is added if
                    // - the participating entities exists in the diagram.
                    // or
                    // - the participating entities will be added in the current transaction.
                    participatingEntityTypes.Add(derivedEntityType);
                    participatingEntityTypes.Add(((ConceptualEntityType)derivedEntityType).BaseType.Target);

                    foreach (var entityType in participatingEntityTypes)
                    {
                        if (diagram.EntityTypeShapes.Where(ets => ets.EntityType.Target == entityType).Any()
                            || shapeChangeInfoSet.Where(
                                sc =>
                                sc.DiagramId == diagram.Id.Value && sc.ModelEFObject == entityType && sc.ChangeType == XObjectChange.Add)
                                   .Any())
                        {
                            continue;
                        }
                        // If it reach this point, that means that we should not add inheritance connector in the diagram.
                        return;
                    }
                    var cmd = new CreateInheritanceConnectorCommand(diagram, derivedEntityType);
                    CommandProcessor.InvokeSingleCommand(commandProcessorContext, cmd);
                }
            }
            else if (changeAction == XObjectChange.Remove
                     && shapeChangeInfoExists == false)
            {
                // this is happening before the transaction is taking place so we are free to look up anti-dependencies
                // on this delete
                var owningEntityType = baseType.Parent as EntityType;
                if (owningEntityType != null)
                {
                    foreach (
                        var inheritanceConnector in
                            owningEntityType.GetAntiDependenciesOfType<InheritanceConnector>()
                                .Where(ic => ic.Diagram != null && ic.Diagram.Id == diagram.Id.Value))
                    {
                        if (inheritanceConnector != null)
                        {
                            var deleteInheritanceConnectorCommand = inheritanceConnector.GetDeleteCommand();
                            CommandProcessor.InvokeSingleCommand(commandProcessorContext, deleteInheritanceConnectorCommand);
                        }
                    }
                }
            }
        }

        #endregion
    }
}
